﻿using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;

using WebImageResizer.Utils;

namespace WebImageResizer.ImageTools
{
	public class NativeImageTool : IImageTool
	{
		#region IImageTool Members

		public virtual Bitmap Resize(Bitmap source, int? width, int? height, bool preservePerspective)
		{
			double newWidth = width.HasValue ? Convert.ToDouble(width.Value) : 0;
			double newHeight = height.HasValue ? Convert.ToDouble(height.Value) : 0;

			if (preservePerspective)
			{
				if (newWidth > 0 && newHeight <= 0)
				{
					newHeight = (newWidth/source.Width)*source.Height;
				}
				else if (newHeight > 0 && newWidth <= 0)
				{
					newWidth = (newHeight/source.Height)*source.Width;
				}
			}

			if (newHeight <= 0)
			{
				newHeight = 1;
			}

			if (newWidth <= 0)
			{
				newWidth = 1;
			}

			PixelFormat format = source.PixelFormat;
			Bitmap bitmap = new Bitmap((int) newWidth, (int) newHeight, format);
			using (Graphics graphics = Graphics.FromImage(bitmap))
			{
				graphics.CompositingMode = CompositingMode.SourceOver;
				graphics.CompositingQuality = CompositingQuality.GammaCorrected;
				graphics.SmoothingMode = SmoothingMode.HighQuality;
				graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
				graphics.DrawImage(source, 0, 0,
					Convert.ToInt32(newWidth), Convert.ToInt32(newHeight));
				return bitmap;
			}
		}

		public virtual Bitmap Zoom(Bitmap source, float zoomFactor)
		{
			int width = Convert.ToInt32(source.Width*zoomFactor);
			int height = Convert.ToInt32(source.Height*zoomFactor);
			return Resize(source, width, height, true);
		}

		public virtual Bitmap ToGreyScale(Bitmap source)
		{
			Bitmap tempBitmap = new Bitmap(source, source.Width, source.Height);

			using (Graphics newGraphics = Graphics.FromImage(tempBitmap))
			{
				float[][] floatColorMatrix = {
					new[] {.3f, .3f, .3f, 0, 0},
					new[] {.59f, .59f, .59f, 0, 0},
					new[] {.11f, .11f, .11f, 0, 0},
					new float[] {0, 0, 0, 1, 0},
					new float[] {0, 0, 0, 0, 1}
				};

				ColorMatrix newColorMatrix = new ColorMatrix(floatColorMatrix);
				using (ImageAttributes attributes = new ImageAttributes())
				{
					attributes.SetColorMatrix(newColorMatrix);
					newGraphics.DrawImage(tempBitmap,
						new Rectangle(0, 0, tempBitmap.Width, tempBitmap.Height),
						0, 0, tempBitmap.Width, tempBitmap.Height, GraphicsUnit.Pixel, attributes);
				}
			}

			return tempBitmap;
		}

		public virtual byte[] Encode(Bitmap source, ImageFormat imageFormat)
		{
			using (MemoryStream ms = new MemoryStream())
			{
				source.Save(ms, imageFormat);
				return ms.ToArray();
			}
		}

		public virtual Bitmap Rotate(Bitmap source, float angle)
		{
			if (source.IsNull())
			{
				throw new ArgumentNullException("source");
			}

			const double pi2 = Math.PI/2.0D;
			double oldWidth = source.Width;
			double oldHeight = source.Height;

			// Convert degrees to radians
			double theta = angle*Math.PI/180.0D;
			double lockedTheta = theta;

			// Ensure theta is now [0, 2pi)
			while (lockedTheta < 0.0D)
			{
				lockedTheta += 2.0D*Math.PI;
			}

			#region Explaination of the calculations

			/*
			 * The trig involved in calculating the new width and height
			 * is fairly simple; the hard part was remembering that when 
			 * PI/2 <= theta <= PI and 3PI/2 <= theta < 2PI the width and 
			 * height are switched.
			 * 
			 * When you rotate a rectangle, r, the bounding box surrounding r
			 * contains for right-triangles of empty space.  Each of the 
			 * triangles hypotenuse's are a known length, either the width or
			 * the height of r.  Because we know the length of the hypotenuse
			 * and we have a known angle of rotation, we can use the trig
			 * function identities to find the length of the other two sides.
			 * 
			 * sine = opposite/hypotenuse
			 * cosine = adjacent/hypotenuse
			 * 
			 * solving for the unknown we get
			 * 
			 * opposite = sine * hypotenuse
			 * adjacent = cosine * hypotenuse
			 * 
			 * Another interesting point about these triangles is that there
			 * are only two different triangles. The proof for which is easy
			 * to see, but its been too long since I've written a proof that
			 * I can't explain it well enough to want to publish it.  
			 * 
			 * Just trust me when I say the triangles formed by the lengths 
			 * width are always the same (for a given theta) and the same 
			 * goes for the height of r.
			 * 
			 * Rather than associate the opposite/adjacent sides with the
			 * width and height of the original bitmap, I'll associate them
			 * based on their position.
			 * 
			 * adjacent/oppositeTop will refer to the triangles making up the 
			 * upper right and lower left corners
			 * 
			 * adjacent/oppositeBottom will refer to the triangles making up 
			 * the upper left and lower right corners
			 * 
			 * The names are based on the right side corners, because thats 
			 * where I did my work on paper (the right side).
			 * 
			 * Now if you draw this out, you will see that the width of the 
			 * bounding box is calculated by adding together adjacentTop and 
			 * oppositeBottom while the height is calculate by adding 
			 * together adjacentBottom and oppositeTop.
			 */

			#endregion

			double adjacentTop, oppositeTop;
			double adjacentBottom, oppositeBottom;

			// We need to calculate the sides of the triangles based
			// on how much rotation is being done to the bitmap.
			//   Refer to the first paragraph in the explaination above for 
			//   reasons why.
			if ((lockedTheta >= 0.0D && lockedTheta < pi2) ||
				(lockedTheta >= Math.PI && lockedTheta < (Math.PI + pi2)))
			{
				adjacentTop = Math.Abs(Math.Cos(lockedTheta))*oldWidth;
				oppositeTop = Math.Abs(Math.Sin(lockedTheta))*oldWidth;

				adjacentBottom = Math.Abs(Math.Cos(lockedTheta))*oldHeight;
				oppositeBottom = Math.Abs(Math.Sin(lockedTheta))*oldHeight;
			}
			else
			{
				adjacentTop = Math.Abs(Math.Sin(lockedTheta))*oldHeight;
				oppositeTop = Math.Abs(Math.Cos(lockedTheta))*oldHeight;

				adjacentBottom = Math.Abs(Math.Sin(lockedTheta))*oldWidth;
				oppositeBottom = Math.Abs(Math.Cos(lockedTheta))*oldWidth;
			}

			double newWidth = adjacentTop + oppositeBottom;
			double newHeight = adjacentBottom + oppositeTop;

			int nWidth = (int) newWidth;
			int nHeight = (int) newHeight;

			Bitmap rotatedBmp = new Bitmap(nWidth, nHeight);

			using (Graphics g = Graphics.FromImage(rotatedBmp))
			{
				g.PixelOffsetMode = PixelOffsetMode.HighQuality;
				g.SmoothingMode = SmoothingMode.HighQuality;
				g.InterpolationMode = InterpolationMode.HighQualityBicubic;

				// This array will be used to pass in the three points that 
				// make up the rotated source
				PointF[] points;

				/*
				 * The values of opposite/adjacentTop/Bottom are referring to 
				 * fixed locations instead of in relation to the
				 * rotating source so I need to change which values are used
				 * based on the how much the source is rotating.
				 * 
				 * For each point, one of the coordinates will always be 0, 
				 * nWidth, or nHeight.  This because the Bitmap we are drawing on
				 * is the bounding box for the rotated bitmap.  If both of the 
				 * corrdinates for any of the given points wasn't in the set above
				 * then the bitmap we are drawing on WOULDN'T be the bounding box
				 * as required.
				 */
				if (lockedTheta >= 0.0D && lockedTheta < pi2)
				{
					points = new[]
						{
							new PointF((float) oppositeBottom, 0.0F),
							new PointF((float) newWidth, (float) oppositeTop),
							new PointF(0.0F, (float) adjacentBottom)
						};
				}
				else if (lockedTheta >= pi2 && lockedTheta < Math.PI)
				{
					points = new[]
						{
							new PointF((float) newWidth, (float) oppositeTop),
							new PointF((float) adjacentTop, (float) newHeight),
							new PointF((float) oppositeBottom, 0.0F)
						};
				}
				else if (lockedTheta >= Math.PI && lockedTheta < (Math.PI + pi2))
				{
					points = new[]
						{
							new PointF((float) adjacentTop, (float) newHeight),
							new PointF(0.0F, (float) adjacentBottom),
							new PointF((float) newWidth, (float) oppositeTop)
						};
				}
				else
				{
					points = new[]
						{
							new PointF(0.0F, (float) adjacentBottom),
							new PointF((float) oppositeBottom, 0.0F),
							new PointF((float) adjacentTop, (float) newHeight)
						};
				}

				g.DrawImage(source, points);
			}
			return rotatedBmp;
		}

		#endregion
	}
}